<?php

namespace paymch\models;

use Yii;
use Alipay\aop\AopClient;
use common\models\trade\Trade;
use common\models\trade\TradeOrder;
use yii\db\Exception;
use yii\helpers\VarDumper;
use common\helpers\ArrayHelper;

/**
 * AlipayPayment is the model behind the alipay async notify form.
 * 
 * @author emhome <emhome@163.com>
 * @since 2.0
 */
class AlipayPayment extends BasePaymentModel {

    /**
     * 通知时间
     * @var date 通知的发送时间
     * @example 2015-14-27 15:45:58 格式为yyyy-MM-dd HH:mm:ss
     * @static 是
     */
    public $notify_time;

    /**
     * 通知类型
     * @var string(64) 通知的类型
     * @example trade_status_sync
     * @static 是
     */
    public $notify_type;

    /**
     * 通知校验ID
     * @var string(128) 通知校验ID
     * @example ac05099524730693a8b330c5ecf72da9786
     * @static 是
     */
    public $notify_id;

    /**
     * 支付宝分配给开发者的应用Id
     * @var string(32) 支付宝分配给开发者的应用Id
     * @example 2014072300007148
     * @static 是
     */
    public $app_id;

    /**
     * 编码格式
     * @var string(10) 编码格式，如utf-8、gbk、gb2312等
     * @example utf-8
     * @static 是
     */
    public $charset;

    /**
     * 接口版本
     * @var string(3) 调用的接口版本，固定为：1.0
     * @example 1.0
     * @static 是
     */
    public $version;

    /**
     * 签名类型
     * @var string(10) 商户生成签名字符串所使用的签名算法类型，目前支持RSA2和RSA，推荐使用RSA2
     * @example RSA2
     * @static 是
     */
    public $sign_type;

    /**
     * 签名
     * @var string(256) 请参考异步返回结果的验签
     * @example 601510b7970e52cc63db0f44997cf70e
     * @static 是
     */
    public $sign;

    /**
     * 支付宝交易号
     * @var string(64) 支付宝交易凭证号
     * @example 2013112011001004330000121536
     * @static 是
     */
    public $trade_no;

    /**
     * 商户订单号
     * @var string(32) 原支付请求的商户订单号
     * @example 6823789339978248
     * @static 是
     */
    public $out_trade_no;

    /**
     * 商户退款业务号
     * @var string(32) 商户业务ID，主要是退款通知中返回退款申请的流水号
     * @example HZRF001
     * @static 否
     */
    public $out_biz_no;

    /**
     * 买家支付宝用户号
     * @var string(16) 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字
     * @example 2088102122524333
     * @static 否
     */
    public $buyer_id;

    /**
     * 买家支付宝账号
     * @var string(100) 买家支付宝账号
     * @example 15901825620
     * @static 否
     */
    public $buyer_logon_id;

    /**
     * 卖家支付宝用户号
     * @var string(30) 卖家支付宝用户号
     * @example 2088101106499364
     * @static 否
     */
    public $seller_id;

    /**
     * 卖家支付宝账号
     * @var string(100) 卖家支付宝账号
     * @example zhuzhanghu@alitest.com
     * @static 否
     */
    public $seller_email;

    /**
     * 交易状态
     * @var string(32) 交易目前所处的状态，见交易状态说明
     * @example TRADE_CLOSED
     * @static 否
     */
    public $trade_status;

    /**
     * 订单金额
     * @var decimal(9,2) 本次交易支付的订单金额，单位为人民币（元）
     * @example 20
     * @static 否
     */
    public $total_amount;

    /**
     * 实收金额
     * @var decimal(9,2) 商家在交易中实际收到的款项，单位为元
     * @example 15
     * @static 否
     */
    public $receipt_amount;

    /**
     * 开票金额
     * @var decimal(9,2) 用户在交易中支付的可开发票的金额
     * @example 10
     * @static 否
     */
    public $invoice_amount;

    /**
     * 付款金额
     * @var decimal(9,2) 用户在交易中支付的金额
     * @example 13.88
     * @static 否
     */
    public $buyer_pay_amount;

    /**
     * 集分宝金额
     * @var decimal(9,2) 使用集分宝支付的金额
     * @example 12
     * @static 否
     */
    public $point_amount;

    /**
     * 总退款金额
     * @var decimal(9,2) 退款通知中，返回总退款金额，单位为元，支持两位小数
     * @example 2.58
     * @static 否
     */
    public $refund_fee;

    /**
     * 订单标题
     * @var string(256) 商品的标题/交易标题/订单标题/订单关键字等，是请求时对应的参数，原样通知回来
     * @example 当面付交易
     * @static 否
     */
    public $subject;

    /**
     * 商品描述
     * @var string(400) 该订单的备注、描述、明细等。对应请求时的body参数，原样通知回来
     * @example 当面付交易内容
     * @static 否
     */
    public $body;

    /**
     * 交易创建时间
     * @var date 该笔交易创建的时间,格式为yyyy-MM-dd HH:mm:ss
     * @example 2015-04-27 15:45:22
     * @static 否
     */
    public $gmt_create;

    /**
     * 交易付款时间
     * @var date 该笔交易的买家付款时间。格式为yyyy-MM-dd HH:mm:ss
     * @example 2015-04-27 15:45:22
     * @static 否
     */
    public $gmt_payment;

    /**
     * 交易退款时间
     * @var date 该笔交易的退款时间。格式为yyyy-MM-dd HH:mm:ss.S
     * @example 2015-04-28 15:45:57.320
     * @static 否
     */
    public $gmt_refund;

    /**
     * 交易结束时间
     * @var date 该笔交易结束时间。格式为yyyy-MM-dd HH:mm:ss
     * @example 2015-04-28 15:45:57.320
     * @static 否
     */
    public $gmt_close;

    /**
     * 支付金额信息
     * @var string(512) 支付成功的各个渠道金额信息，详见资金明细信息说明
     * @example [{“amount”:“15.00”,“fundChannel”:“ALIPAYACCOUNT”}]
     * @static 否
     */
    public $fund_bill_list;

    /**
     * 回传参数
     * @var string(512) 公共回传参数，如果请求时传递了该参数，则返回给商户时会在异步通知时将该参数原样返回。本参数必须进行UrlEncode之后才可以发送给支付宝
     * @example merchantBizType%3d3C%26merchantBizNo%3d20160101011
     * @static 否
     */
    public $passback_params;

    /**
     * 优惠券信息
     * @var string 本交易支付时所使用的所有优惠券信息，详见优惠券信息说明
     * @example [{“amount”:“0.20”,“merchantContribute”:“0.00”,“name”:“一键创建券模板的券名称”,“otherContribute”:“0.20”,“type”:“ALIPAY_DISCOUNT_VOUCHER”,“memo”:“学生卡8折优惠”]
     * @static 否
     */
    public $voucher_detail_list;

    /**
     * 优惠券信息
     * @var string 本交易支付时所使用的所有优惠券信息，详见优惠券信息说明
     * @example [{“amount”:“0.20”,“merchantContribute”:“0.00”,“name”:“一键创建券模板的券名称”,“otherContribute”:“0.20”,“type”:“ALIPAY_DISCOUNT_VOUCHER”,“memo”:“学生卡8折优惠”]
     * @static 否
     */
    public $auth_app_id;

    /**
     * 优惠券信息
     * @var type $_trade
     */
    private $_trade = null;

    /**
     * 支付宝回调通知响应参数校验
     * @inheritdoc
     *
     * 1、商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号，
     * 2、判断total_amount是否确实为该订单的实际金额（即商户订单创建时的金额），
     * 3、校验通知中的seller_id（或者seller_email) 是否为out_trade_no这笔单据的对应的操作方（有的时候，一个商户可能有多个seller_id/seller_email），
     * 4、验证app_id是否为该商户本身。
     * 上述1、2、3、4有任何一个验证不通过，则表明本次通知是异常通知，务必忽略。
     */
    public function rules() {
        return [
            // 必填内容
            [['notify_time', 'notify_type', 'notify_id', 'app_id', 'charset', 'version', 'sign_type', 'sign', 'trade_no', 'out_trade_no'], 'required'],
            // 校验基本格式信息
            [['notify_type', 'trade_no'], 'string', 'max' => 64],
            [['out_trade_no', 'out_biz_no'], 'string', 'max' => 32],
            [['charset', 'sign_type'], 'string', 'max' => 10],
            [['notify_id'], 'string', 'max' => 128],
            [['app_id', 'trade_status'], 'string', 'max' => 32],
            [['version'], 'string', 'max' => 3],
            ['sign', 'string'],
            ['auth_app_id', 'safe'],
            [['subject'], 'string', 'max' => 256],
            [['buyer_id'], 'string', 'max' => 16],
            [['buyer_logon_id', 'seller_email'], 'string', 'max' => 100],
            [['seller_id'], 'string', 'max' => 30],
            [['total_amount', 'receipt_amount', 'invoice_amount', 'buyer_pay_amount', 'point_amount', 'refund_fee'], 'number'],
            [['body'], 'string', 'max' => 400],
            [['notify_time', 'gmt_create', 'gmt_payment', 'gmt_close'], 'datetime'],
            [['gmt_refund'], 'date', 'format' => 'yyyy-MM-dd HH:mm:ss.S'],
            [['fund_bill_list', 'passback_params'], 'string', 'max' => 512],
            [['voucher_detail_list'], 'string'],
            //校验APPID
            ['app_id', 'validateAppId'],
            //验证支付宝请求签名
            ['sign', 'validateSignature'],
            //校验支付回调通知状态
            ['trade_status', 'in', 'range' => ['TRADE_FINISHED', 'TRADE_SUCCESS', 'TRADE_CLOSED']],
            //校验商户订单号
            [['out_biz_no', 'out_trade_no'], 'validateTradeNo'],
            //校验订单支付金额
            ['total_amount', 'validateTotalAmount'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels() {
        return [
            'notify_time' => '通知时间',
            'notify_type' => '通知类型',
            'notify_id' => '通知校验ID',
            'app_id' => '支付宝分配给开发者的应用Id',
            'charset' => '编码格式',
            'version' => '接口版本',
            'sign_type' => '签名类型',
            'sign' => '签名',
            'trade_no' => '支付宝交易号',
            'out_trade_no' => '商户订单号',
            'out_biz_no' => '商户业务号',
            'buyer_id' => '买家支付宝用户号',
            'buyer_logon_id' => '买家支付宝账号',
            'seller_id' => '卖家支付宝用户号',
            'seller_email' => '卖家支付宝账号',
            'trade_status' => '交易状态',
            'total_amount' => '订单金额',
            'receipt_amount' => '实收金额',
            'invoice_amount' => '开票金额',
            'buyer_pay_amount' => '付款金额',
            'point_amount' => '集分宝金额',
            'refund_fee' => '总退款金额',
            'subject' => '订单标题',
            'body' => '商品描述',
            'gmt_create' => '交易创建时间',
            'gmt_payment' => '交易付款时间',
            'gmt_refund' => '交易退款时间',
            'gmt_close' => '交易结束时间',
            'fund_bill_list' => '支付金额信息',
            'passback_params' => '回传参数',
            'voucher_detail_list' => '优惠券信息',
        ];
    }

    /**
     * Validates the APPID.
     * 验证app_id是否为该商户本身
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateAppId($attribute) {
        if (!$this->hasErrors()) {
            if ($this->app_id !== $this->getPartnerParams('appid')) {
                $this->addError($attribute, '非当前应用支付商户！');
            }
        }
    }

    /**
     * Validates the signature.
     * 校验请求的签名
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateSignature($attribute) {
        if (!$this->hasErrors()) {
            $aop = new AopClient;
            $aop->alipayrsaPublicKey = $this->getPartnerParams('public');
//            $params = $this->attributes;
            $params = Yii::$app->request->post();
            $flag = $aop->rsaCheckV1($params, NULL, "RSA2");
            if (!$flag) {
                $this->addError($attribute, '请求签名校验失败！');
            }
        }
    }

    /**
     * Validates the out_trade_no.
     * 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateTradeNo($attribute) {
        if (!$this->hasErrors()) {
            $trade = $this->getTradeOrder();
            if ($trade == null) {
                $this->addError($attribute, '商户订单号不存在！' . $this->$attribute);
            }
        }
    }

    /**
     * Validates the total_amount.
     * 判断total_amount是否确实为该订单的实际金额（即商户订单创建时的金额）
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateTotalAmount($attribute) {
        if (!$this->hasErrors()) {
            if (!$this->out_biz_no) {
                $trade = $this->getTradeOrder();
                if (!$trade || $trade->pay != $this->total_amount) {
                    $this->addError($attribute, '商户订单的支付金额不一致！');
                }
            }
        }
    }

    protected function getPartnerParams($key) {
        return ArrayHelper::getValue(Yii::$app->params, [
            'alipay',
            $this->app_id,
            $key
        ]);
    }

    /**
     * Finds trade by [[trade_no]]
     *
     * @return Trade|null
     */
    protected function getTradeOrder() {
        if ($this->_trade === null) {
            if ($this->out_biz_no) {
                //退款时
                $this->_trade = TradeOrder::find()->alias('t')->joinWith('trade trade')->where([
                    'trade.trade_no' => $this->out_biz_no,
                    't.paymode' => TradeOrder::PAYMODE_ALIPAY,
                    't.balance' => TradeOrder::BALANCE_REFUND,
                ])->one();
            } else {
                //付款时
                $this->_trade = TradeOrder::findByTn($this->out_trade_no, TradeOrder::PAYMODE_ALIPAY);
            }
            if (!$this->_trade) {
                $record = [
                    'out_biz_no' => $this->out_biz_no,
                    'out_trade_no' => $this->out_trade_no,
                ];
                Yii::error('未找到相关的交易订单号：' . ArrayHelper::toString($record));
            }
        }
        return $this->_trade;
    }

    /**
     * 
     * Sends an email to the specified email address using the information collected by this model.
     *
     * 在上述验证通过后商户必须根据支付宝不同类型的业务通知，正确的进行不同的业务处理，并且过滤重复的通知结果数据。
     * 在支付宝的业务通知中，只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时，支付宝才会认定为买家付款成功。
     * 
     * TRADE_FINISHED 	交易完成 	true（触发通知）
     * TRADE_SUCCESS 	支付成功 	true（触发通知）
     * WAIT_BUYER_PAY 	交易创建 	false（不触发通知）
     * TRADE_CLOSED 	交易关闭 	true（触发通知）
     * 
     * WAIT_BUYER_PAY 	交易创建，等待买家付款
     * TRADE_CLOSED 	未付款交易超时关闭，或支付完成后全额退款
     * TRADE_SUCCESS 	交易支付成功
     * TRADE_FINISHED 	交易结束，不可退款
     * 
     * @param string $email the target email address
     * @return bool whether the email was sent
     */
    public function save(&$msg) {
        Yii::info("\n Attributes is：" . VarDumper::export($this->attributes), 'payment');
        if (!$this->validate()) {
            $msg = VarDumper::export($this->firstErrors);
            return false;
        }

        $trade = $this->_trade;
        if (!$trade) {
            $msg = '交易订单不存在！';
            return false;
        }

        //已支付或者是已完成
        $trade->paymode = TradeOrder::PAYMODE_ALIPAY;

        if ($this->out_biz_no && $this->gmt_refund) {
            $trade->pay_no = $this->trade_no;
            $trade->pay_time = (string) strtotime($this->gmt_refund);
            if ($this->trade_status === 'TRADE_CLOSED') {
                $trade->status = TradeOrder::STATUS_SUCCESS;
            }
        } else {
            $trade->pay_no = $this->trade_no;
            $trade->pay_time = (string) strtotime($this->gmt_payment);
        }

        //开启事务
        $transaction = Yii::$app->db->beginTransaction();
        try {
            if ($this->trade_status === 'TRADE_SUCCESS') {
                $trade->status = TradeOrder::STATUS_SUCCESS;
            } elseif ($this->trade_status === 'TRADE_FINISHED') {
                $trade->status = TradeOrder::STATUS_SUCCESS;
            } elseif ($this->trade_status === 'TRADE_CLOSED') {
                $trade->setMark('交易关闭');
            }
            if (!$trade->save()) {
                throw new Exception('保存失败！' . print_r($trade->errors, true));
            }
            $transaction->commit();
        } catch (Exception $e) {
            $transaction->rollBack();
            $msg = $e->getMessage();
            return false;
        }

        return $trade->trade_id;
    }

}
